using System;
using System.IO;
using System.Collections;
using System.Collections.Generic;
using Refly.CodeDom;
using Refly.CodeDom.Statements;
using Refly.CodeDom.Collections;
using Refly.Templates;
using Refly.CodeDom.Expressions;
using System.Text;
using System.Text.RegularExpressions;
using TestCoverage;
using ASOGS.Patterns;
using Reflector.Framework;
using System.ComponentModel;
using Doubler.Data;
using Doubler.Helpers;
using Doubler.Helpers.Graph;

namespace Doubler.Templates.UnitTest
{

    public enum TestFramework
    {
        MbUnit, NUnit, MSTest
    }

    public class TestTemplate : SingleTemplateBase
    {
        
#region Fields

        private IClassDeclaration _TestClass;
        private DoubleDispatchReflector _TestVisitor;
        private Hashtable _CallMap;
        private Hashtable _MethodTreeCache;
        private Boolean _WithTestAttibutes = false;
        private TestFramework _TestFramework = TestFramework.MbUnit;
        private DoubleDispatchReflector _CallVisitor;
        private DoubleDispatchReflector _CallBuilderVisitor;

#endregion
        
#region Properties

        [Browsable(false)]
        public DoubleDispatchReflector CallBuilderVisitor
        {
            get
            {
                if (_CallBuilderVisitor == null)
                    _CallBuilderVisitor = new DoubleDispatchReflector(this, "VisitBuildCallToRecording");
                return _CallBuilderVisitor;
            }
            set
            {
                _CallBuilderVisitor = value;
            }
        }

        [Browsable(false)]
        public DoubleDispatchReflector CallVisitor
        {
            get
            {
                if (_CallVisitor == null)
                    _CallVisitor = new DoubleDispatchReflector(this, "VisitCall");
                return _CallVisitor;
            }
            set
            {
                _CallVisitor = value;
            }
        }

        public TestFramework TestFramework
        {
            get
            {
                return _TestFramework;
            }
            set
            {
                _TestFramework = value;
            }
        }

        public Boolean WithTestAttibutes
        {
            get
            {
                return _WithTestAttibutes;
            }
            set
            {
                _WithTestAttibutes = value;
            }
        }

        private Hashtable MethodTreeCache
        {
            get
            {
                if (_MethodTreeCache == null)
                    _MethodTreeCache = new Hashtable();
                return _MethodTreeCache;
            }
            set
            {
                _MethodTreeCache = value;
            }
        }

        private Hashtable CallMap
        {
            get
            {
                if (_CallMap == null)
                {
                    _CallMap = new Hashtable();
                    this.CreateCallMap();
                }
                return _CallMap;
            }
            set
            {
                _CallMap = value;
            }
        }

        private DoubleDispatchReflector TestVisitor
        {
            get
            {
                if (_TestVisitor == null)
                    _TestVisitor = new DoubleDispatchReflector(this, "VisitTestGenerateMember");
                return _TestVisitor;
            }
            set
            {
                _TestVisitor = value;
            }
        }

        private IClassDeclaration TestClass
        {
            get
            {
                return _TestClass;
            }
            set
            {
                _TestClass = value;
            }
        }

        public override string FileName
        {
            get
            {
                return string.Format(base.NameFormat, this.Subject.Name);
            }
        }

        public override string OutputFilePath
        {
            get
            {
                string NamespacePath = Regex.Replace(this.Namespace, @"\.", "/");
                return string.Format("{0}/{1}/{2}.{3}", this.OutputPath, NamespacePath, this.FileName, this.OutputLanguage);
            }
        }

#endregion

#region Constructor

        public TestTemplate()
            : base("", "Test{0}")
        {
            this.Namespace = string.Format("{0}.{1}", this.Subject.Namespace, "UnitTests");
        }

        public TestTemplate(CodeLanguage language, string outDirectoryPath, Reflector.CodeModel.ITypeDeclaration subject, ReflectorServices reflectorServices)
            : base(language, outDirectoryPath, subject, "Test{0}", reflectorServices)
        {
            this.Namespace = string.Format("{0}.{1}", this.Subject.Namespace, "UnitTests");
        }

#endregion
        
#region Generation

        public override void Generate()
        {
            this.Prepare();
            this.GenerateTest();
            this.Compile();
        }

        public void GenerateTest()
        {
            this.TestClass = base.NamespaceDeclaration.AddClass(this.FileName);

            this.TestClass.Imports.Add(typeof(TestSubjectClassAttribute).Namespace);
            this.TestClass.Imports.Add("TestDoubles");
            if (this.WithTestAttibutes)
            {
                if (this.TestFramework == TestFramework.MbUnit)
                {
                    this.TestClass.Imports.Add("MbUnit.Framework");
                    this.TestClass.Imports.Add("MbUnit.Core.Framework");
                    this.TestClass.CustomAttributes.Add("TestFixture");
                }
                else if (this.TestFramework == TestFramework.NUnit)
                {
                    this.TestClass.Imports.Add("NUnit.Framework");
                    this.TestClass.CustomAttributes.Add("TestFixture");
                }
                else if (this.TestFramework == TestFramework.MSTest)
                {
                    this.TestClass.Imports.Add("Microsoft.VisualStudio.TestTools.UnitTesting");
                    this.TestClass.CustomAttributes.Add("TestClass");
                }
            }

            AttributeDeclaration TestSubjectClass = this.TestClass.CustomAttributes.Add(typeof(TestSubjectClassAttribute));
            TestSubjectClass.Arguments.Add("TestSubject", Expr.TypeOf(new StringTypeDeclaration(MetaHelper.GetFullTypeName(this.Subject))));
            this.GenerateTestsForPublicMembers();
            this.GenerateTestsForNonPublicMembers();
        }

        private void GenerateTestsForPublicMembers()
        {
            VisibilityConfiguration Visibility = new VisibilityConfiguration();
            Visibility.Public = true;
            ArrayList Members = new ArrayList();
            Members.AddRange(Reflector.Helper.GetMethods( this.Subject, Visibility));

            foreach (Reflector.CodeModel.IMemberDeclaration MemberInformation in Members)
            {
                if (MetaHelper.IsNotNestedMember(MemberInformation.Name)
                        && this.Subject == ((Reflector.CodeModel.ITypeReference)MemberInformation.DeclaringType).Resolve()
                        && !MemberInformation.Name.EndsWith(".ctor"))
                    this.AcceptTestMember(MemberInformation, MemberInformation.Name);
            }
        }

        public void GenerateTestsForNonPublicMembers()
        {
            VisibilityConfiguration Visibility = new VisibilityConfiguration();
            Visibility.FamilyOrAssembly = true;
            ArrayList Members = new ArrayList();
            Members.AddRange(Reflector.Helper.GetMethods(this.Subject, Visibility));

            foreach (Reflector.CodeModel.IMethodDeclaration MethodInformation in Members)
            {
                if (MetaHelper.IsNotNestedMember(MethodInformation.Name)
                        && this.Subject == MethodInformation.DeclaringType
                        && this.CallMap.Contains(MethodInformation.Name)
                        && MethodInformation.Virtual
                        && !MethodInformation.Name.EndsWith(".ctor"))
                {
                    Reflector.CodeModel.IMethodDeclaration CallingMember = (Reflector.CodeModel.IMethodDeclaration)this.CallMap[MethodInformation.Name];
                    this.AcceptTestMember(CallingMember, MethodInformation.Name);
                }
            }
        }

        public void AcceptTestMember(Reflector.CodeModel.IMemberDeclaration memberInformation, String testSubjectMemberName)
        {
            try
            {
                Object[] Args = new Object[2] { memberInformation, testSubjectMemberName };
                this.TestVisitor.Invoke(Args);
            }
            catch (Exception ex)
            {
                Logging.LogClassFactory.Logger.Log(ex.ToString());
            }
        }

#endregion
        
#region Test Method Generation

        public void VisitTestGenerateMember(Reflector.CodeModel.IMethodDeclaration methodInformation, String testSubjectMemberName)
        {
            MethodDeclaration Method = this.TestClass.AddMethod(methodInformation.Name);
            
            AttributeDeclaration TestSubjectMember = Method.CustomAttributes.Add(typeof(TestSubjectMemberAttribute));
            TestSubjectMember.Arguments.Add("MemeberName", Expr.Prim(testSubjectMemberName));
            if (this.WithTestAttibutes)
            {
                if (this.TestFramework == TestFramework.MSTest)
                {
                    Method.CustomAttributes.Add("TestMethod");
                }
                else
                {
                    Method.CustomAttributes.Add("Test");
                }
            }

            VariableDeclarationStatement TestSubjectVar = new VariableDeclarationStatement(new StringTypeDeclaration(MetaHelper.GetFullTypeName(this.Subject)), "TestSubject");

            Expression TestSubjectVarRef = Expr.Var(TestSubjectVar);
            Expression[] ConstructorArguments;

            Reflector.CodeModel.IMethodDeclaration TransformedMethod = this.Services.Disassembler.TranslateMethodDeclaration(methodInformation);
            CallGraphBuilder Builder = new CallGraphBuilder(TransformedMethod);
            CallGraph Graph = Builder.ConstructGraph();

            StatementCollection AssertStatements = new StatementCollection();

            Boolean HasPublicConstructor = false;
            Reflector.CodeModel.IMethodDeclaration Constructor = null;
            foreach (Reflector.CodeModel.IMethodDeclaration PossibleConstructor in this.Subject.Methods)
            {
                if (PossibleConstructor.Name.EndsWith(".ctor"))
                {
                    Constructor = PossibleConstructor;
                    HasPublicConstructor = true;
                    break;
                }
            }

            if (HasPublicConstructor)
            {
                Method.Body.Add(Stm.Comment("Create Constructor Parameters"));
                ConstructorArguments = this.CreateArgumentList(Constructor, Method.Body, ref AssertStatements, Graph);
                Method.Body.Add(Stm.Snippet(string.Empty));
                Method.Body.Add(Stm.Var(TestSubjectVar.Type, TestSubjectVar.Name, Expr.New(MetaHelper.GetFullTypeName(this.Subject), ConstructorArguments)));
                Method.Body.Add(Stm.Snippet(string.Empty));
            }
            else
            {
                Method.Body.Add(Stm.Snippet(string.Empty));
                Method.Body.Add(Stm.Comment("No public constructors were found, you need to get an instance of the test subject assigned to the variable TestSubject."));
                Method.Body.Add(TestSubjectVar);
                Method.Body.Add(Stm.Snippet(string.Empty));
            }

            NativeMethodReferenceExpression MethodToTest = TestSubjectVarRef.Method(methodInformation.Name);
            if (methodInformation.Parameters.Count > 0)
                Method.Body.Add(Stm.Comment("Create Test Method Parameters"));
            Expression[] TestMethodArguments = this.CreateArgumentList(methodInformation, Method.Body, ref AssertStatements, Graph);
            this.CreatePropertyAssertions(TestSubjectVarRef , Method.Body, ref AssertStatements, Graph);
            NativeMethodInvokeExpression InvokedMethodToTest = MethodToTest.Invoke(TestMethodArguments);
            Method.Body.Add(Stm.Snippet(string.Empty));
            Method.Body.Add(InvokedMethodToTest);
            Method.Body.Add(Stm.Snippet(string.Empty));
            Method.Body.Add(Stm.Comment(string.Empty));
            Method.Body.Add(Stm.Comment("Write your assertions here."));
            Method.Body.Add(Stm.Comment(string.Empty));

            Method.Body.AddRange(AssertStatements);
        }

#endregion

        public void CreatePropertyAssertions(Expression testSubjectRef, StatementCollection body, ref StatementCollection assertStatements, CallGraph callGraph)
        {
            foreach (KeyValuePair<Reflector.CodeModel.IPropertyReferenceExpression, CallsFromProperty> Pair in callGraph.FromProperties)
            {
                foreach (CallChain Chain in Pair.Value.CallChains)
                {
                    Reflector.CodeModel.IMethodReferenceExpression MethodRef = (Reflector.CodeModel.IMethodReferenceExpression)Chain.LastCall.Call;
                    Reflector.CodeModel.IMethodDeclaration MethodCalled = MethodRef.Method.Resolve();

                    Expression Assertion;
                    string RecordingName = Recorder.RecorderGenerator.GetRecordingName(MethodCalled);

                    Expression Recorders = testSubjectRef.Prop(Recorder.RecorderGenerator.RecordingsPropertyName);

                    Expression MemberCall = this.AcceptBuildCallToRecording(Recorders, Chain);

                    Expression Recording = MemberCall.Prop(RecordingName);

                    if (MethodCalled.ReturnType.Type.ToString() != "Void" && MethodCalled.ReturnType.Type.ToString() != "System.Void")
                    {
                        Statement SetReturn = this.MakeSetRecorderReturnValue(Recording, MethodCalled.ReturnType);
                        body.Add(SetReturn);
                    }

                    foreach (Reflector.CodeModel.IParameterDeclaration Parameter in MethodCalled.Parameters)
                    {
                        Assertion = this.MakeAssertParameterPassed(Recording, Parameter);
                        assertStatements.Add(Assertion);
                    }

                    Assertion = this.MakeAssertCalled(Recording);
                    assertStatements.Add(Assertion);
                }
            }
            foreach (Reflector.CodeModel.IPropertyReferenceExpression propertyReferenceExpression in callGraph.PropertiesAssigned)
            {
                NativeMethodReferenceExpression AssertToProperty = Expr.Type("Assert").Method("AreEqual");
                Expression ReplaceNote = Expr.Prim("<Please replace with valid value.>");
                NativeMethodInvokeExpression Assertion = AssertToProperty.Invoke(new Expression[2] { ReplaceNote, testSubjectRef.Prop(propertyReferenceExpression.Property.Name) });
                assertStatements.Add(Assertion);
            }
        }

#region Argument Creation

        private Expression[] CreateArgumentList(Reflector.CodeModel.IMethodDeclaration methodBaseInfo, StatementCollection body, ref StatementCollection assertStatements, CallGraph callGraph)
        {
            Expression ParameterVar;
            ArrayList TempArray = new ArrayList();
            foreach (Reflector.CodeModel.IParameterDeclaration ParameterInformation in methodBaseInfo.Parameters)
            {
                ParameterVar = this.CreateArgument(ParameterInformation, body, ref assertStatements, callGraph);
                TempArray.Add(ParameterVar);
            }
            Expression[] Parameters;
            Parameters = (Expression[])TempArray.ToArray(typeof(Expression));
            return Parameters;
        }

        private Expression MakeAssertCalled(Expression recording)
        {
            NativeMethodReferenceExpression AssertToCall = Expr.Type("Assert").Method("IsTrue");
            Expression Called = recording.Prop(Recorder.RecorderGenerator.RecordingCalled);
            NativeMethodInvokeExpression AssertInvoked = AssertToCall.Invoke(new Expression[1] { Called });
            return AssertInvoked;
        }

        public Statement MakeSetRecorderReturnValue(Expression recording, Reflector.CodeModel.IMethodReturnType returnType)
        {
            Expression ReturnValue = recording.Prop(Recorder.RecorderGenerator.RecordingReturnValue);
            return Stm.Assign(ReturnValue, Expr.Prim("Please set the return value."));
        }

        public Expression MakeAssertParameterPassed(Expression recording, Reflector.CodeModel.IParameterDeclaration parameter)
        {
            NativeMethodReferenceExpression AssertToPassedParam = Expr.Type("Assert").Method("AreEqual");
            Expression PassedRecording = recording.Prop(Recorder.RecorderGenerator.GetPassedRecordingName(parameter.ParameterType.ToString(), parameter.Name));
            Expression ReplaceNote = Expr.Prim("<Please replace with valid value.>");
            NativeMethodInvokeExpression AssertInvoked = AssertToPassedParam.Invoke(new Expression[2] { ReplaceNote, PassedRecording });
            return AssertInvoked;
        }

        public Expression CreateArgument(Reflector.CodeModel.IParameterDeclaration parameterInformation, StatementCollection body, ref StatementCollection assertStatements, CallGraph callGraph)
        {
            VariableDeclarationStatement ParameterVar;
            VariableReferenceExpression ParameterVarRef;
            if (((Reflector.CodeModel.ITypeReference)parameterInformation.ParameterType).Resolve().Abstract)
            {
                String RecorderTypeName = Recorder.RecorderGenerator.GetRecorderClassTypeName(parameterInformation.ParameterType.ToString());
                ParameterVar = new VariableDeclarationStatement(new StringTypeDeclaration(RecorderTypeName), String.Format("{0}Recording", parameterInformation.Name));
                ParameterVarRef = Expr.Var(ParameterVar);
                body.Add(Stm.Var(ParameterVar.Type, ParameterVar.Name, Expr.New(RecorderTypeName, new Expression[0])));

                foreach (KeyValuePair<Reflector.CodeModel.IArgumentReferenceExpression, CallsFromArgument> Pair in callGraph.FromArguments)
                {
                    if (Pair.Key.Parameter.Name == parameterInformation.Name)
                    {
                        foreach (CallChain Chain in Pair.Value.CallChains)
                        {
                            Reflector.CodeModel.IMethodReferenceExpression MethodRef = (Reflector.CodeModel.IMethodReferenceExpression)Chain.LastCall.Call;
                            Reflector.CodeModel.IMethodDeclaration MethodCalled = MethodRef.Method.Resolve();

                            Expression Assertion;
                            string RecordingName = Recorder.RecorderGenerator.GetRecordingName(MethodCalled);

                            Expression Recorders = ParameterVarRef.Prop(Recorder.RecorderGenerator.RecordingsPropertyName);

                            Expression MemberCall = this.AcceptBuildCallToRecording(Recorders, Chain);
                            
                            Expression Recording = MemberCall.Prop(RecordingName);

                            if (MethodCalled.ReturnType.Type.ToString() != "Void" && MethodCalled.ReturnType.Type.ToString() != "System.Void")
                            {
                                Statement SetReturn = this.MakeSetRecorderReturnValue(Recording, MethodCalled.ReturnType);
                                body.Add(SetReturn);
                            }

                            foreach (Reflector.CodeModel.IParameterDeclaration Parameter in MethodCalled.Parameters)
                            {
                                Assertion = this.MakeAssertParameterPassed(Recording, Parameter);
                                assertStatements.Add(Assertion);
                            }

                            Assertion = this.MakeAssertCalled(Recording);
                            assertStatements.Add(Assertion);
                        }
                    }
                }
            }
            else
            {
                ParameterVar = new VariableDeclarationStatement(new StringTypeDeclaration(MetaHelper.GetFullTypeName(parameterInformation.ParameterType)), parameterInformation.Name);
                ParameterVarRef = Expr.Var(ParameterVar);

                Reflector.CodeModel.IMethodDeclaration Constructor = null;
                foreach (Reflector.CodeModel.IMethodDeclaration PossibleConstructor in ((Reflector.CodeModel.ITypeReference)parameterInformation.ParameterType).Resolve().Methods)
                {
                    if (PossibleConstructor.Name.EndsWith(".ctor"))
                    {
                        Constructor = PossibleConstructor;
                        break;
                    }
                }

                if (Constructor != null && Constructor.Parameters.Count == 0)
                {
                    body.Add(Stm.Var(ParameterVar.Type, ParameterVar.Name, Expr.New(MetaHelper.GetFullTypeName(parameterInformation.ParameterType), new Expression[0])));
                }
                else
                {
                    body.Add(Stm.Snippet(string.Empty));
                    body.Add(Stm.Comment(string.Format("There is no default constuctor for the parameter {0} type {1}.", parameterInformation.Name, ((Reflector.CodeModel.ITypeReference)parameterInformation.ParameterType).Name)));
                    body.Add(ParameterVar);
                    body.Add(Stm.Snippet(string.Empty));
                }
            }
            return ParameterVarRef;
        }
        
#endregion

#region Sense and Control

        public void VisitCall(Reflector.CodeModel.IArgumentReferenceExpression argumentReferenceExpression, CallsFromArgument callsFromArgument)
        {
            
        }

        public void VisitCall(Reflector.CodeModel.IPropertyReferenceExpression propertyReferenceExpression, CallsFromProperty callsFromProperty)
        {
        	
        }

        public void VisitCall(Reflector.CodeModel.IFieldReferenceExpression fieldReferenceExpression, CallsFromField callsFromField)
        {
        	
        }
    
        public void VisitCall(Reflector.CodeModel.IVariableReferenceExpression variableReferenceExpression, CallsFromVariable callsFromVariable)
        {
        	
        }
    
        public void VisitCall(Reflector.CodeModel.IThisReferenceExpression thisReferenceExpression, CallsFromThis callsFromThis)
        {
        	
        }
    
        public void VisitCall(Reflector.CodeModel.ITypeReferenceExpression typeReferenceExpression, CallsFromStatic callsFromStatic)
        {
        	
        }

#endregion

#region Call Map Helpers

        public void CreateCallMap()
        {
            try
            {
                Reflector.CodeModel.IMethodDeclarationCollection Methods = this.Subject.Methods;
                this.ExamineMethods(Methods);
                Methods = this.Services.Search.FindType(string.Format("{0}.{1}", this.Subject.BaseType.Namespace, this.Subject.BaseType.Name)).Methods;
                this.ExamineMethods(Methods);
            }
            catch (Exception ex)
            {
                System.Diagnostics.Debug.WriteLine(ex.ToString());
            }
        }

        private void ExamineMethods(Reflector.CodeModel.IMethodDeclarationCollection Methods)
        {
            foreach (Reflector.CodeModel.IMethodDeclaration Method in Methods)
            {
                if (Method.Name.EndsWith(".ctor") || Method.Visibility != Reflector.CodeModel.MethodVisibility.Public)
                    continue;

                foreach (Reflector.CodeModel.IMethodDeclaration MethodCalled in MetaHelper.GetCallsOut(Method, this.MethodTreeCache))
                {
                    Reflector.CodeModel.ITypeReference OwningType = (Reflector.CodeModel.ITypeReference) MethodCalled.DeclaringType;

                    if (OwningType.Resolve() == this.Subject || OwningType.Resolve() == this.Subject.BaseType.Resolve())
                    {
                        if (!this.CallMap.Contains(MethodCalled.Name))
                        {
                            this.CallMap.Add(MethodCalled.Name, Method);
                        }
                    }
                }
            }
        }

#endregion

        public Expression AcceptBuildCallToRecording(Expression expression, CallChain chain)
        {
            try
            {
                Object[] Args = new Object[2] { expression, chain };
                return (Expression)this.CallBuilderVisitor.Invoke(Args);
            }
            catch (Exception ex)
            {
                Logging.LogClassFactory.Logger.Log(ex.ToString());
            }
            return expression;
        }

        public Expression VisitBuildCallToRecording(Expression expression, PropertyCallChain chain)
        {
            Expression Call = expression.Prop(chain.Call.Property.Name);
            if (chain.Chain != null)
            {
                return this.AcceptBuildCallToRecording(Call, chain.Chain);
            }
            else
            {
                return Call;
            }
        }

        public Expression VisitBuildCallToRecording(Expression expression, MethodCallChain chain)
        {
            return expression;
        }

        public Expression VisitBuildCallToRecording(Expression expression, FieldCallChain chain)
        {
            Expression Call = expression.Prop(chain.Call.Field.Name);
            if (chain.Chain != null)
            {
                return this.AcceptBuildCallToRecording(Call, chain.Chain);
            }
            else
            {
                return Call;
            }
        }

    }
}
